package com.aboverock.integrations.fileserver.service;

import com.aboverock.integrations.fileserver.domain.File;
import com.aboverock.integrations.fileserver.domain.FileData;
import com.aboverock.integrations.fileserver.repository.FileDataRepository;
import com.aboverock.integrations.fileserver.repository.FileRepository;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.Binary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

/**
 * <p>
 * FileService
 * <p>
 *
 * @author RockerWang
 * @date Created in 2021/12/2 23:29
 */
@Service
@Slf4j
public class FileService {

    /**
     * 默认的 chunk size 大小 8MB
     */
    private static final int DEFAULT_CHUNK_SIZE = 1024 * 1024 * 8;

    @Autowired
    public FileRepository fileRepository;

    @Autowired
    private FileDataRepository fileDataRepository;

    /**
     * 获取文件信息
     *
     * @param id 文件id
     * @return 文件信息
     */
    public Optional<File> getFileById(String id) {
        return fileRepository.findById(id);
    }

    /**
     * 获取所有文件信息.
     *
     * @param limit  the limit
     * @param offset the offset
     * @return 文件信息列表
     */
    public List<File> listFiles(Integer limit, Integer offset) {
        if (limit == null) {
            limit = 200;
        }
        if (offset == null) {
            offset = 0;
        }
        int pageSize = limit;
        int current = offset / pageSize;
        return listFilesByPage(current, pageSize);
    }

    /**
     * 获取分页文件信息
     *
     * @param current  当前页码
     * @param pageSize 每页数量
     * @return 文件信息列表
     */
    public List<File> listFilesByPage(Integer current, Integer pageSize) {
        if (current == null) {
            current = 0;
        }
        if (pageSize == null) {
            pageSize = 10;
        }
        Sort sort = new Sort(Sort.Direction.DESC, "uploadDate");
        Pageable pageable = PageRequest.of(current, pageSize, sort);

        Page<File> page = fileRepository.findAll(pageable);
        return page.getContent();
    }

    /**
     * 获取文件数据
     *
     * @param fileId   文件id
     * @param response http请求对象
     */
    public void getFileData(String fileId, HttpServletResponse response) {
        if (fileId == null || fileId.isEmpty()) {
            return;
        }

        // 去除文件扩展名
        int index = fileId.indexOf('.');
        if (index != -1) {
            fileId = fileId.substring(0, index);
        }

        long start = System.currentTimeMillis();
        try (OutputStream os = response.getOutputStream()) {
            Optional<File> orFile = getFileById(fileId);
            if (orFile.isPresent()) {
                File file = orFile.get();
                String fileName = URLEncoder.encode(orFile.get().getName(), "UTF-8")
                        .replaceAll("\\+", "%20");
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; fileName*=UTF-8''" + fileName);
                response.setHeader(HttpHeaders.CONTENT_TYPE, "application/octet-stream");
                response.setHeader(HttpHeaders.CONTENT_LENGTH, file.getSize() + "");
                response.setHeader("Connection", "close");
                List<FileData> fileDataList = fileDataRepository.findByFileId(fileId);
                fileDataList.sort(Comparator.comparing(FileData::getSequence));
                long end1 = System.currentTimeMillis();
                log.info("get file: {} data from mongo(ms): {}", fileId, (end1 - start));

                for (FileData fileData : fileDataList) {
                    os.write(fileData.getData().getData());
                }
                os.flush();
                long end2 = System.currentTimeMillis();
                log.info("send file: {} data finish(ms): {}", fileId, (end2 - start));

                file.setDownloadCount(file.getDownloadCount() + 1);
                file.setLatestDownload(new Date());
                fileRepository.save(file);
            } else {
                response.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
                response.setStatus(HttpStatus.NOT_FOUND.value());
                os.write("File was not found".getBytes());
            }
        } catch (Exception ex) {
            log.error("getFileData not error", ex);
            response.setStatus(HttpStatus.NOT_FOUND.value());
        }
    }

    /**
     * 将文件内容写入到OutputStream中
     *
     * @param fileId 文件id
     * @param os     OutputStream输出流
     */
    public void getFileData(String fileId, OutputStream os) {
        long start = System.currentTimeMillis();
        Optional<File> orFile = getFileById(fileId);
        if (orFile.isPresent()) {
            List<FileData> fileDataList = fileDataRepository.findByFileId(fileId);
            fileDataList.sort(Comparator.comparing(FileData::getSequence));
            long end1 = System.currentTimeMillis();
            log.info("get data file: {} data from mongo(ms): {}", fileId, (end1 - start));

            try {
                for (FileData fileData : fileDataList) {
                    os.write(fileData.getData().getData());
                }
                os.flush();
                long end2 = System.currentTimeMillis();
                log.info("send data file: {} data finish(ms): {}", fileId, (end2 - start));

                File file = orFile.get();
                file.setDownloadCount(file.getDownloadCount() + 1);
                file.setLatestDownload(new Date());
                fileRepository.save(file);
            } catch (IOException e) {
                log.error("get file data error", e);
            }
        }
    }

    /**
     * 根据文件id获取字节数组
     *
     * @param fileId MongoDB数据库的文件id
     * @return 文件内容的字节数组
     */
    public byte[] getFileDataAsBytes(String fileId) {
        Optional<File> orFile = getFileById(fileId);
        if (!orFile.isPresent()) {
            return new byte[0];
        }

        List<FileData> fileDataList = fileDataRepository.findByFileId(fileId);
        fileDataList.sort(Comparator.comparing(FileData::getSequence));

        int available = fileDataList.stream().mapToInt(FileData::getDataSize).reduce(Integer::sum).orElse(0);
        int offset = 0;
        byte[] byteBuf = new byte[available];
        for (FileData fileData : fileDataList) {
            int len = fileData.getData().getData().length;
            System.arraycopy(fileData.getData().getData(), 0, byteBuf, offset, len);
            offset = offset + len;
        }
        return byteBuf;
    }

    /**
     * 上传文件
     * 文件会按照 DEFAULT_CHUNK_SIZE 大小分片存储在MongoDB中
     *
     * @param file 上传文件对象
     * @return 文件保存结果
     */
    public File uploadFile(MultipartFile file) {
        long start = System.currentTimeMillis();
        log.info("upload file start: {} at {}", file.getOriginalFilename(), new Date());
        try {
            File f = new File();
            f.setName(file.getOriginalFilename());
            f.setContentType(file.getContentType());
            f.setSize(file.getSize());
            f.setUploadDate(new Date());
            f.setChunkSize(DEFAULT_CHUNK_SIZE);
            File returnFile = fileRepository.save(f);

            InputStream is = file.getInputStream();
            int chunkCount = 0;
            int byteRead;
            byte[] buf = new byte[DEFAULT_CHUNK_SIZE];
            while ((byteRead = is.read(buf)) != -1) {
                saveFileData(returnFile.getId(), chunkCount, byteRead, buf);
                chunkCount += 1;
            }
            returnFile.setChunkCount(chunkCount);
            returnFile.setPath("/files/" + returnFile.getId());
            returnFile.setDataPath("/files/data/" + returnFile.getId());
            returnFile = fileRepository.save(returnFile);
            long end1 = System.currentTimeMillis();
            log.info("upload file end: {} at {} fileId: {} and spend(ms): {}",
                    file.getOriginalFilename(), new Date(), returnFile.getId(), (end1 - start));

            return returnFile;
        } catch (IOException ex) {
            log.error("upload file error", ex);
            return null;
        }
    }

    /**
     * 保存java文件到服务器
     *
     * @param javaFile java文件对象
     * @return 保存结果
     */
    public File saveJavaFile(java.io.File javaFile) {
        long start = System.currentTimeMillis();
        log.info("upload file start: {} at {}", javaFile.getName(), new Date());

        File f = new File();
        f.setName(javaFile.getName());
        f.setContentType(getFileSuffix(javaFile.getName()));
        f.setSize(javaFile.length());
        f.setUploadDate(new Date());
        f.setChunkSize(DEFAULT_CHUNK_SIZE);
        File returnFile = fileRepository.save(f);

        try (InputStream is = new FileInputStream(javaFile)) {
            byte[] buf = new byte[DEFAULT_CHUNK_SIZE];
            int chunkCount = 0;
            int byteRead;
            while ((byteRead = is.read(buf)) != -1) {
                saveFileData(returnFile.getId(), chunkCount, byteRead, buf);
                chunkCount += 1;
            }
            returnFile.setDataPath("/files/data/" + returnFile.getId());
            returnFile.setPath("/files/" + returnFile.getId());
            returnFile.setChunkCount(chunkCount);
            returnFile = fileRepository.save(returnFile);
            long end1 = System.currentTimeMillis();
            log.info("save file end: {} at {} fileId: {} and spend(ms): {}",
                    javaFile.getName(), new Date(), returnFile.getId(), (end1 - start));
            return returnFile;
        } catch (IOException ex) {
            log.error("save file error", ex);
            return null;
        }
    }

    /**
     * 复制文件
     *
     * @param fileId 文件id
     * @return 复制结果
     */
    public File copyFile(String fileId) {
        Optional<File> orFile = getFileById(fileId);
        if (orFile.isPresent()) {
            List<FileData> fileDataList = fileDataRepository.findByFileId(fileId);
            fileDataList.sort(Comparator.comparing(FileData::getSequence));

            File f = new File();
            f.setName(orFile.get().getName());
            f.setContentType(orFile.get().getContentType());
            f.setSize(orFile.get().getSize());
            f.setUploadDate(new Date());
            f.setChunkSize(DEFAULT_CHUNK_SIZE);
            final File returnFile = fileRepository.save(f);

            fileDataList.forEach(data -> {
                FileData fileData = new FileData();
                fileData.setFileId(returnFile.getId());
                fileData.setSequence(data.getSequence());
                fileData.setDataSize(data.getDataSize());
                fileData.setData(data.getData());
                fileDataRepository.insert(fileData);
            });

            return returnFile;
        }
        return null;
    }

    /**
     * 删除文件.
     *
     * @param id 文件id
     * @return the string
     */
    public String deleteFile(String id) {
        log.info("delete file: {}", id);
        fileRepository.deleteById(id);
        fileDataRepository.deleteByFileId(id);
        return "{\"code\":0,\"message\":\"success\",\"data\":\"" + id + "\"}";
    }

    /**
     * 保存文件数据块
     *
     * @param fileId     文件id
     * @param chunkCount 当前数据块序号
     * @param byteRead   读取的数据长度
     * @param buf        数据块内容
     */
    private void saveFileData(String fileId, int chunkCount, int byteRead, byte[] buf) {
        FileData fileData = new FileData();
        fileData.setFileId(fileId);
        fileData.setSequence(chunkCount);

        // 保存的数据长度小于buf长度则拷贝到新数组
        if (byteRead < buf.length) {
            byte[] readBuf = new byte[byteRead];
            System.arraycopy(buf, 0, readBuf, 0, byteRead);
            fileData.setDataSize(readBuf.length);
            fileData.setData(new Binary(readBuf));
        } else {
            fileData.setDataSize(buf.length);
            fileData.setData(new Binary(buf));
        }

        fileDataRepository.insert(fileData);
    }

    /**
     * 获取文件后缀名
     *
     * @param fileName 文件名
     * @return 文件后缀名
     */
    private String getFileSuffix(String fileName) {
        if (fileName == null || fileName.isEmpty()) {
            return "";
        }
        int index = fileName.lastIndexOf('.');
        if (index == -1 || index == fileName.length() - 1) {
            return "";
        }
        return fileName.substring(index + 1);
    }
}
